Moving a character with a walk cycle on a path with these requirements?
I figured it out. I wrote a Python script (pretty rough, but it works) where I set the name of the curve object, the distance for a full stride, how many frames it takes to go a full stride, the start frame, and the desired end frame. Then it'll print out the values needed for the Follow Path constraint's Offset parameter. If I put in too many frames and the calculations happened to overshoot the distance on the path (so much that the Offset value is greater than 1), then the script will show a different end frame where we are as close to under 1 as possible.
I've tested this and my character walks perfectly along the path -- no sliding at all. It's exactly what I was looking to do. As I thought about it, I could make the character follow different paths simply by reducing the influence of the Follow Path constraint as needed. (Of course, I would have to re-position the character so it would look seamless, but it would be possible. I may not end up doing this, though...it's probably more hassle than it's worth.)
I plan to improve this script, where it'll automatically insert the keyframes in the Follow Path constraint (as well as provide an option to not start at the very beginning of the path), but this is sufficient for now.
Here's the code in case if anyone finds it useful. Note that this expects to be run from Blender 2.8's Scripting workspace, where a Python Console window is present. I also used some code from MacKracken's Arc Length for the curve distance calculations.
from console_python import get_console
from contextlib import redirect_stdout
from mathutils import *
from bpy.props import *
import bpy
import math

###############################################################################
# Calculate Path Offset Value
#
# Figure out the Follow Path constraint's Offset value that we need in order to
# use a walk cycle along a path.

PATH_NAME = 'NurbsPath'                     # The name of the path we want to walk
FULL_STRIDE = 0.25                          # How far is our full stride in Blender units?
FRAMES_TO_GO_FULL_STRIDE = 13               # How many frames do we need to go per one full stride?
FRAME_START = 101                           # When do we start walking on the path?
FRAME_END = 520                             # When do we stop walking on the path?


###############################################################################
# Version History
#
# 1.0.0 - 2020-08-03: Original version
#

###############################################################################
SCRIPT_NAME = &quot;calculate_path_offset_value&quot;
###############################################################################
    
def add_scrollback(override, text, text_type): # Necessary for printing to the console
    append = bpy.ops.console.scrollback_append
    
    for l in text.split(&quot;\n&quot;):
    txt = l.replace(&quot;\t&quot;, &quot;    &quot;)
    append(override, text=txt, type=text_type)

def print_msg(text): # Necessary for printing to the console
    con = stdout = stderr = None
    for area in bpy.context.screen.areas:    
    if area.type == 'CONSOLE':
        hsh = hash(area.regions[-1])
        con, stdout, stderr = get_console(hsh)
        break

    stdout.seek(0)
    stdout.truncate(0)

    with redirect_stdout(stdout):
    con.push('print(&quot;' + text + '&quot;)')

    out = stdout.getvalue()
    if out:
    override = {'area': area, 'region': area.regions[-1], 'space_data': area.spaces.active}
    add_scrollback(override, out, 'OUTPUT')

#calcs a nurbs knot vector
def knots(n, order, type=0):#0 uniform 1 endpoints 2 bezier
    kv = []    
    t = n+order
    if type==0:
    for i in range(0, t):
        kv.append(1.0*i)

    elif type==1:
    k=0.0
    for i in range(1, t+1):
        kv.append(k)
        if i&gt;=order and i&lt;=n:
        k+=1.0
    elif type==2:
    if order==4:
        k=0.34
        for a in range(0,t):
        if a&gt;=order and a&lt;=n: k+=0.5
        kv.append(floor(k))
        k+=1.0/3.0
    
    elif order==3:
        k=0.6
        for a in range(0, t):
        if a &gt;=order and a&lt;=n: k+=0.5
        kv.append(floor(k)) 
    
    ##normalize the knot vector
    for i in range(0, len(kv)):
    kv[i]=kv[i]/kv[-1]
    
    return kv

#nurbs curve evaluation
def C(t, order, points, weights, knots):
    #c = Point([0,0,0])
    c = Vector()
    rational = 0
    i = 0
    while i &lt; len(points):
    b = B(i, order, t, knots)
    p = points[i] * (b * weights[i])
    c = c + p
    rational = rational + b*weights[i]
    i = i + 1
    
    return c * (1.0/rational)

#nurbs basis function
def B(i,k,t,knots):
    ret = 0
    if k&gt;0:
    n1 = (t-knots[i])*B(i,k-1,t,knots)
    d1 = knots[i+k] - knots[i]
    n2 = (knots[i+k+1] - t) * B(i+1,k-1,t,knots)
    d2 = knots[i+k+1] - knots[i+1]
    if d1 &gt; 0.0001 or d1 &lt; -0.0001:
        a = n1 / d1
    else:
        a = 0
    if d2 &gt; 0.0001 or d2 &lt; -0.0001:
        b = n2 / d2
    else:
        b = 0
    ret = a + b
    #print &quot;B i = %d, k = %d, ret = %g, a = %g, b = %g\n&quot;%(i,k,ret,a,b)
    else:
    if knots[i] &lt;= t and t &lt;= knots[i+1]:
        ret = 1
    else:
        ret = 0
    return ret

#gets nurbs polygon control points on global coordinates
def getnurbspoints(spl, mw):
    pts = []
    ws = []
    for p in spl.points:
    v = Vector(p.co[0:3]) @ mw
    pts.append(v)
    ws.append(p.weight)
    return pts , ws

#calculates a global parameter t along all control points
#t=0 begining of the curve
#t=1 ending of the curve
def calct(obj, t):    
    spl=None
    mw = obj.matrix_world
    if obj.data.splines.active==None:
    if len(obj.data.splines)&gt;0:
        spl=obj.data.splines[0]
    else:
    spl = obj.data.splines.active
    
    if spl==None:
    return False
    
    if spl.type==&quot;BEZIER&quot;:
    points = spl.bezier_points
    nsegs = len(points)-1
    
    d = 1.0/nsegs
    seg = int(t/d)
    t1 = t/d - int(t/d)
    
    if t==1:
        seg-=1
        t1 = 1.0
    
    p = getbezpoints(spl,mw, seg)
    
    coord = cubic(p, t1)
    
    return coord

    elif spl.type==&quot;NURBS&quot;:
    data = getnurbspoints(spl, mw)
    pts = data[0]
    ws = data[1]
    order = spl.order_u
    n = len(pts)
    ctype = spl.use_endpoint_u
    kv = knots(n, order, ctype)
    
    coord = C(t, order-1, pts, ws, kv)
    
    return coord


def get_path_length(obj):
    length = 0.0
    
    if obj.type==&quot;CURVE&quot;:
    prec = 1000 #precision
    inc = 1/prec #increments
    
    ### TODO: set a custom precision value depending the number of curve points
    #that way it can gain on accuracy in less operations.
    
    #subdivide the curve in 1000 lines and sum its magnitudes
    for i in range(0, prec):
        ti = i*inc 
        tf = (i+1)*inc 
        a = calct(obj, ti) 
        b = calct(obj, tf)
        r = (b-a).magnitude 
        length+=r
    
    return length

    
###############################################################################
# Main program
###############################################################################

print_msg('**********************************')
print_msg(SCRIPT_NAME + ' - START')

path_length = get_path_length(bpy.data.objects[PATH_NAME])
stride_length = FULL_STRIDE / path_length
total_frames = (FRAME_END - FRAME_START) 
end_value = (total_frames / FRAMES_TO_GO_FULL_STRIDE) * stride_length

print_msg('  ----- RESULTS -----')
print_msg('    ' + str(PATH_NAME) + ' has a length of ' + str(path_length))
print_msg('    One full stride length for a Follow Path constraint Offset: ' + str(stride_length))

if (end_value &gt; 1.0):
    print_msg('    *** WARNING! INPUT PARAMETERS OVERSHOOT THE OFFSET VALUE! ***')
    print_msg('    *** We are going to modify which frame where the animation ends so that we can end with a useable Offset value. ***')
    while (end_value &gt; 1.0):
    end_value -= stride_length / FRAMES_TO_GO_FULL_STRIDE
    FRAME_END -= 1

print_msg('    FRAME ' + str(FRAME_START) + ' Offset: 0')
print_msg('    FRAME ' + str(FRAME_END) + ' Offset: ' + str(end_value))
    
print_msg('  -------------------')
print_msg(SCRIPT_NAME + ' - END')
print_msg('**********************************')
print_msg('Done running script ' + SCRIPT_NAME)